"""
ForgeFed plugin for Pagure.
Copyright (C) 2020-2021 zPlus <zplus@peers.community>

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License along
with this program; if not, see <https://www.gnu.org/licenses/>.

SPDX-FileCopyrightText:  2020-2021 zPlus <zplus@peers.community>
SPDX-License-Identifier: GPL-2.0-only
"""

import celery
import json
import pagure.lib.query
import rdflib
import re
import requests
import requests_http_signature

from .. import APP_URL
from .. import activitypub
from .. import feeds
from .. import model
from .. import settings
from . import broker_url
from . import broker
from . import database_session

log = celery.utils.log.get_task_logger(__name__)
log.setLevel(settings.LOG_LEVEL)

# The following is a decorator that accepts a dictionary as input and adds it to
# _PATTERNS_ together with the decorated function. The dictionary is used as a
# pattern for matching incoming Activities. If an incoming Activity matches one
# of the patters in _PATTERNS_, then the corresponding function is executed.
_PATTERNS_ = []
def pattern(activity_pattern):
    def closure(func):

        def decorator(*args, **kwargs):
            func(*args, **kwargs)

        global _PATTERNS_
        _PATTERNS_.append((activity_pattern, decorator))

        return decorator
    return closure

# The firs one is used to match *any* value in the Activity, or in other words to
# make sure that the key exists irregardless of its value.
# The other two are used to match a local or remote object only
_       = activitypub.Document.AnyType()
_remote = re.compile('^(?!'+APP_URL+')')
_local  = re.compile('^'+APP_URL)

@broker.task
def perform(activity):
    """
    This task is responsible for accepting an incoming Activity after it's been
    validated (see tasks.delivery) and decide what to do with it.

    :param activity: the Activity that was sent to the Actor
    """

    activity = activitypub.Activity.from_dict(activity)

    for activity_pattern, function in _PATTERNS_:
        if activity.match(activity_pattern):
            with database_session() as database:
                return function(database, activity)

    log.debug('Activity did not match any pattern. Ignoring incoming Activity: {}'.format(
        json.dumps(activity['id'], indent=4, sort_keys=True)))




# Repository
# -----------------------------------------------------------------------------

@pattern({
    'type': 'Create',
    'id': _,
    'actor': {
        'id': _
    },
    'object': {
        'type': 'TagRef',
        'id': _,
        'name': _,
        'context': {
            'type': 'Repository',
            'id': _
        }
    },
})
def create_repository_tag(database, activity):
    """
    Somebody has Created a Tag reference in a repository.
    """

    log.debug('Handling incoming Create(TagRef) Activity.')

    tagref = activity['object']
    repository = tagref['context']

    # Get the list of Actors that are Following the Repository
    items = database \
        .query(model.Collection) \
        .filter(model.Collection.uri == repository['followers']) \
        .all()

    # Only the local Actor
    followers = [ result.item for result in items if result.item.startswith(APP_URL) ]

    # Add a feed to recipients
    for follower in followers:
        actor = model.from_uri(database, follower)

        if not actor:
            continue

        database.add(model.Feed(actor.uri, feeds.create_tagref(repository, tagref)))




# Follow
# -----------------------------------------------------------------------------

@pattern({
    'type': 'Follow',
    'id': _,
    'actor': {
        'id': _
    },
    'object': {
        'id': _
    },
})
def follow(database, activity):
    """
    An Actor has Followed another Actor.
    """

    log.debug('Handling incoming Follow Activity.')

    followed = model.from_uri(database, activity['object']['id'])

    if not followed:
        log.debug('Followed Actor {} does not exist on this instance.'.format(followed.uri))
        return

    if followed.is_remote:
        log.debug('Ignoring Follow Activity {} received for a remote Actor {}'.format(
            activity['id'], activity['object']['id']))
        return

    # Check if the local actor is already following the remote actor
    if database.query(
            database.query(model.Collection) \
                    .filter(model.Collection.uri == activity['object']['following']) \
                    .filter(model.Collection.item == activity['actor']['id']) \
                    .exists()
        ).scalar():

        log.info('Actor {} is already following {}. Ignoring Follow request.'.format(followed.uri, follower.uri))
        return

    # Automatically Accept any Follow request
    if followed.is_local:
        # Update the followers/following collections

        database.merge(model.Collection(
            uri  = activity['object']['following'],
            item = activity['actor']['id']))

        database.merge(model.Collection(
            uri  = activity['actor']['following'],
            item = activity['object']['id']))

        database.merge(model.Collection(
            uri  = activity['object']['followers'],
            item = activity['actor']['id']))

        database.merge(model.Collection(
            uri  = activity['actor']['followers'],
            item = activity['object']['id']))

        # Send the Accept Activity
        activitypub.Activity(
            type   = 'Accept',
            actor  = followed.uri,
            object = activity['id']
        ).distribute()

        # Cache the remote Actor
        database.merge(model.Resource(
            uri = activity['actor']['id'],
            document = json.dumps(activity['actor'])))

        # Add a feed
        database.add(model.Feed(followed.uri, feeds.follow(activity['actor'], activity['object'])))
        database.add(model.Feed(followed.uri, feeds.accept_follow(activity['actor'], activity['object'])))

@pattern({
    'type': 'Accept',
    'id': _,
    'actor': {
        'id': _
    },
    'object': {
        'type': 'Follow',
        'id': _,
        'actor': {
            'id': _
        },
        'object': {
            'id': _
        }
    },
})
def accept_follow(database, activity):
    """
    Somebody has Accepted of a Follow request.
    """

    log.debug('Handling incoming Accept(Follow) Activity.')

    follow = activity['object']

    if follow['object']['id'] != activity['actor']['id']:
        log.debug('Only target Actor can Accept a Follow request.\n{}'.format(json.dumps(activity, indent=4)))
        return

    log.debug('{} has accepted Follow request from {}'.format(
        follow['object']['id'], follow['actor']['id']))

    # We cache a copy of the Actors for quick lookups. This is used for
    # example when listing "following/followers" collections. If we only
    # stored the actors' URI we would need to GET the JSON data every time
    # for getting its name.
    database.merge(model.Resource(
        uri = follow['actor']['id'],
        document = json.dumps(activity['actor'])))

    database.merge(model.Resource(
        uri = follow['object']['id'],
        document = json.dumps(follow['object'])))

    # Update the followers/following collections

    database.merge(model.Collection(
        uri  = follow['actor']['following'],
        item = follow['object']['id']))

    database.merge(model.Collection(
        uri  = follow['object']['following'],
        item = follow['actor']['id']))

    database.merge(model.Collection(
        uri  = follow['actor']['followers'],
        item = follow['object']['id']))

    database.merge(model.Collection(
        uri  = follow['object']['followers'],
        item = follow['actor']['id']))

    # If the follower is local
    if follow['actor']['id'].startswith(APP_URL):
        database.add(model.Feed(follow['actor']['id'],
                                feeds.accept_follow(follow['actor'], follow['object'])))




# Ticket
# -----------------------------------------------------------------------------

@pattern({
    'type': 'Create',
    'id': _,
    'actor': {
        'id': _
    },
    'object': {
        'type': 'Ticket',
        'id': _,
        'context': _,
        'attributedTo': _,
        'summary': _,
        'content': _
    },
})
def create_ticket(database, activity):
    """
    Remote user has created a new Ticket for one of our TicketTracker.
    """

    log.debug('Handling incoming Create(Ticket) Activity.')

    ticket = activity['object']
    ticket_tracker = model.from_uri(database, ticket['context'])

    if not ticket_tracker:
        log.debug('Received a new Ticket for nonexistent TicketTracker {}'.format(ticket['context']))
        return

    log.debug('Request to create a new Ticket.')

    # Make sure we have a local actor representing the remote actor
    actor = model.Person.test_or_set(database, ticket['attributedTo'])

    # Create a new local issue in Pagure
    issue = pagure.lib.query.new_issue(
        database,
        ticket_tracker,
        ticket['summary'],
        ticket['content'],
        actor.username)

    if ticket_tracker.is_local:
        # Send an "Accept" request to notify the Actor that the Ticket was
        # Accepted (ticket are accepted automatically)
        activitypub.Activity(
            type  = 'Accept',
            actor = ticket_tracker.local_uri,
            object = activity['id'],
            result = issue.uri
        ).distribute()
    else:
        # If it's a remote issue, we just need to keep track of it (we don't
        # need to Accept anything)
        issue.remote_uri = ticket['id']

@pattern({
    'type': 'Accept',
    'id': _,
    'actor': {
        'id': _
    },
    'object': {
        'type': 'Create',
        'id': _,
        'actor': {},
        'object': {
            'type': 'Ticket',
            'id': _,
            'context': _,
            'attributedTo': _,
            'summary': _,
            'content': _
        }
    },
    'result': _
})
def accept_ticket(database, activity):
    """
    A Ticket was Accepted.
    """

    log.debug('Handling incoming Accept(Create(Ticket)) Activity.')

    ticket = activity['object']['object']

    database.add(model.SameAs(
        local_uri = ticket['id'],
        remote_uri = activity['result']
    ))

    database.add(model.Feed(ticket['attributedTo'], feeds.accept_ticket(activity['actor'], ticket)))

@pattern({
    'type': 'Update',
    'id': _,
    'actor': {
        'id': _
    },
    'object': {
        'type': 'Ticket',
        'id': _local
    },
    'result': {}
})
def update_local_ticket(database, activity):
    """
    Remote user has updated a Ticket belonging to this instance.
    """

    log.debug('Handling incoming Update(Ticket) Activity.')

    remote_ticket = activity['object']
    ticket = model.from_uri(database, remote_ticket['id'])

    if not ticket:
        log.debug('Ticket does not exist: {}'.format(remote_ticket['id']))
        return

    actor = model.Person.test_or_set(database, activity['actor']['id'])
    new_title = activity['result']['summary'] if 'summary' in activity['result'] else None
    new_content = activity['result']['content'] if 'content' in activity['result'] else None

    # Edit the pagure Issue object
    pagure.lib.query.edit_issue(
        session=database,
        issue=ticket,
        user=actor.username,
        title=new_title,
        content=new_content)

    log.debug('Ticket updated: {}'.format(activity['object']['id']))

@pattern({
    'type': 'Update',
    'id': _,
    'actor': {
        'id': _
    },
    'object': {
        'type': 'Ticket',
        'id': _remote
    },
    'result': {}
})
def update_remote_ticket(database, activity):
    """
    Remote user has updated a remote Ticket not belonging to this instance.
    """

    log.debug('Handling incoming Update(Ticket) Activity.')

    remote_ticket = activity['object']
    ticket = model.from_uri(database, remote_ticket['id'])

    if not ticket:
        log.debug('Ticket {} does not exist on this instance.'.format(remote_ticket['id']))
        return

    actor = model.Person.test_or_set(database, activity['actor']['id'])
    new_title = activity['result']['summary'] if 'summary' in activity['result'] else None
    new_content = activity['result']['content'] if 'content' in activity['result'] else None

    # Edit the pagure Issue object
    pagure.lib.query.edit_issue(
        session=database,
        issue=ticket,
        user=actor.username,
        title=new_title,
        content=new_content)

    log.debug('Ticket updated: {}'.format(activity['object']['id']))

@pattern({
    'type': 'Create',
    'id': _,
    'actor': {
        'id': _
    },
    'object': {
        'type': 'Note',
        'id': _remote,
        'context': {
            'id': _,
            'type': 'Ticket'
        },
        'attributedTo': {
            'id': _
        },
        'content': _
    },
})
def create_ticket_comment(database, activity):
    """
    Remote user has created a new TicketComment for one of our TicketTracker.
    """

    log.debug('Handling incoming Create(Note(Ticket)) Activity.')

    note = activity['object']

    # Check if we already have a local object for this remote comment
    if database.query(
           database.query(model.SameAs)
                   .filter(model.SameAs.remote_uri == note['id']).exists()
       ).scalar():

        log.debug('The note {} is already stored in the database. Will not create a new one.'.format(note['id']))
        return

    author = model.Person.test_or_set(database, note['attributedTo']['id'])

    # Check if there is any local Ticket in the pagure database that is used
    # to track this Ticket
    ticket = model.from_uri(database, note['context']['id'])

    if not ticket:
        log.debug('No local Ticket for {}. Ignoring Note.'.format(note['context']['id']))
        return

    # Create the new comment to the ticket
    pagure.lib.query.add_issue_comment(
        session = database,
        issue = ticket,
        comment = note['content'],
        user = author.username)

@pattern({
    'type': 'Resolve',
    'id': _,
    'actor': {
        'id': _
    },
    'object': {
        'type': 'Ticket',
        'id': _local
    },
})
def resolve_local_ticket(database, activity):
    """
    Remote user has closed a Ticket belonging to this instance.
    """

    log.debug('Handling incoming Resolve(Ticket) Activity.')

    ticket = model.from_uri(database, activity['object']['id'])

    if not ticket:
        log.debug('Ticket does not exist: {}'.format(activity['object']['id']))
        return

    actor = model.Person.test_or_set(database, activity['actor']['id'])

    # Edit the pagure Issue object
    pagure.lib.query.edit_issue(
        session=database,
        issue=ticket,
        user=actor.username,
        status='Closed')

    log.debug('Ticket resolved: {}'.format(activity['object']['id']))

@pattern({
    'type': 'Resolve',
    'id': _,
    'actor': {
        'id': _
    },
    'object': {
        'type': 'Ticket',
        'id': _remote
    },
})
def resolve_remote_ticket(database, activity):
    """
    Remote user has closed a remote Ticket not belonging to this instance.
    """

    log.debug('Handling incoming Resolve(Ticket) Activity.')

    ticket = model.from_uri(database, activity['object']['id'])

    # Nothing to do because we don't have any local TicketTracker tracking
    # this remote Ticket
    if not ticket:
        log.debug('Untracked Ticket resolved: {}'.format(activity['object']['id']))
        return

    actor = model.Person.test_or_set(database, activity['actor']['id'])

    # Edit the pagure Issue object
    pagure.lib.query.edit_issue(
        session=database,
        issue=ticket,
        user=actor.username,
        status='Closed')

    log.debug('Ticket resolved: {}'.format(activity['object']['id']))

@pattern({
    'type': 'Reopen',
    'id': _,
    'actor': {
        'id': _
    },
    'object': {
        'type': 'Ticket',
        'id': _local
    },
})
def reopen_local_ticket(database, activity):
    """
    Remote user has reopened a Ticket belonging to this instance.
    """

    log.debug('Handling incoming Reopen(Ticket) Activity.')

    ticket = model.from_uri(database, activity['object']['id'])

    if not ticket:
        log.debug('Ticket does not exist: {}'.format(activity['object']['id']))
        return

    actor = model.Person.test_or_set(database, activity['actor']['id'])

    # Edit the pagure Issue object
    pagure.lib.query.edit_issue(
        session=database,
        issue=ticket,
        user=actor.username,
        status='Open')

    log.debug('Ticket reopened: {}'.format(activity['object']['id']))

@pattern({
    'type': 'Reopen',
    'id': _,
    'actor': {
        'id': _
    },
    'object': {
        'type': 'Ticket',
        'id': _remote
    },
})
def reopen_remote_ticket(database, activity):
    """
    Remote user has reopened a remote Ticket not belonging to this instance.
    """

    log.debug('Handling incoming Reopen(Ticket) Activity.')

    ticket = model.from_uri(database, activity['object']['id'])

    # Nothing to do because we don't have any local TicketTracker tracking
    # this remote Ticket
    if not ticket:
        log.debug('Untracked Ticket reopened: {}'.format(activity['object']['id']))
        return

    actor = model.Person.test_or_set(database, activity['actor']['id'])

    # Edit the pagure Issue object
    pagure.lib.query.edit_issue(
        session=database,
        issue=ticket,
        user=actor.username,
        status='Open')

    log.debug('Ticket reopened: {}'.format(activity['object']['id']))

@pattern({
    'type': 'Delete',
    'id': _,
    'actor': {
        'id': _
    },
    'object': _,
    'origin': {
        'id': _,
        'type': 'TicketTracker'
    }
})
def delete_ticket(database, activity):
    """
    Remote user has deleted a remote Ticket not belonging to this instance.
    """

    log.debug('Handling incoming Delete(Ticket) Activity.')

    object = activity['object']

    if object.startswith(APP_URL):
        log.debug('Actor not allowed to delete local Ticket {}'.format(ticket.uri))
        return

    # The Actor that has deleted the Ticket
    activity_actor = model.Person.test_or_set(database, activity['actor']['id'])

    # Retrieve the local URI of the object
    same_as = database \
            .query(model.SameAs) \
            .filter(model.SameAs.remote_uri == object) \
            .all()

    uri_list = [ row.local_uri for row in same_as ]

    # Delete all remote_uri from the SameAs table
    database \
            .query(model.SameAs) \
            .filter(model.SameAs.remote_uri == object) \
            .delete()

    for local_uri in uri_list:
        ticket = model.from_uri(database, local_uri)
        if not ticket:
            continue

        pagure.lib.query.drop_issue(database, ticket, activity_actor.username)

        log.debug('Deleted Ticket {}'.format(local_uri))




# MergeRequest
# -----------------------------------------------------------------------------

@pattern({
    'type': 'Create',
    'id': _,
    'actor': {
        'id': _
    },
    'object': {
        'type': 'MergeRequest',
        'id': _,
        'attributedTo': _,
        'upstream': {
            'branch': _,
            'repository': {
                'id': _local,
                'type': 'Repository'
            }
        },
        'downstream': {
            'branch': _,
            'repository': {
                'id': _,
                'type': 'Repository'
            }
        },
        'summary': _,
        'content': _,
        'commit_start': _,
        'commit_stop': _,
    },
})
def create_merge_request(database, activity):
    """
    Remote user has created a new MergeRequest for one of our Repositories.
    """

    log.debug('Handling incoming Create(MergeRequest) Activity.')

    mergerequest = activity['object']
    repository = model.from_uri(database, mergerequest['upstream']['repository']['id'])

    if not repository:
        log.debug('Received a new MergeRequest for nonexistent Repository {}'.format(mergerequest['upstream']['id']))
        return

    log.debug('Request to create a new MergeRequest.')

    # First of all, let's pull down the remote repository (synchronously)
    pagure.lib.tasks.pull_remote_repo.apply(kwargs={
        'remote_git': mergerequest['downstream']['repository']['id'],
        'branch_from': mergerequest['downstream']['branch']
    })

    # Make sure we have a local actor representing the remote actor
    actor = model.Person.test_or_set(database, mergerequest['attributedTo'])

    # Create a new local PullRequest in Pagure
    mergerequest = pagure.lib.query.new_pull_request(
        session=database,
        branch_from=mergerequest['downstream']['branch'],
        repo_to=repository,
        branch_to=mergerequest['upstream']['branch'],
        title=mergerequest['summary'],
        user=actor.username,
        initial_comment=mergerequest['content'],
        repo_from=None,
        remote_git=mergerequest['downstream']['repository']['id'],
        commit_start=mergerequest['commit_start'],
        commit_stop=mergerequest['commit_stop'])

    # Send an "Accept" request to notify the remote actor that the
    # MergeRequest was accepted
    activitypub.Activity(
        type  = 'Accept',
        actor = repository.local_uri,
        object = activity['id'],
        result = mergerequest.uri
    ).distribute()

@pattern({
    'type': 'Accept',
    'id': _,
    'actor': {
        'id': _
    },
    'object': {
        'type': 'Create',
        'id': _,
        'actor': {
            'id': _
        },
        'object': {
            'type': 'MergeRequest',
            'id': _,
            'attributedTo': _,
            'upstream': {
                'branch': _,
                'repository': {
                    'id': _,
                    'type': 'Repository'
                }
            },
            'downstream': {
                'branch': _,
                'repository': {
                    'id': _local,
                    'type': 'Repository'
                }
            },
        },
    },
    'result': _
})
def accept_merge_request(database, activity):
    """
    A MergeRequest was Accepted.
    """

    log.debug('Handling incoming Accept(Create(MergeRequest)) Activity.')

    mergerequest = activity['object']['object']

    database.add(model.SameAs(
        local_uri = mergerequest['id'],
        remote_uri = activity['result']
    ))

    database.add(model.Feed(mergerequest['attributedTo'],
                            feeds.accept_mergerequest(mergerequest)))

@pattern({
    'type': 'Update',
    'id': _,
    'actor': {
        'id': _
    },
    'object': {
        'type': 'MergeRequest',
        'id': _local
    },
    'result': {}
})
def update_local_mergerequest(database, activity):
    """
    Remote user has updated a MergeRequest belonging to this instance.
    """

    log.debug('Handling incoming Update(MergeRequest) Activity.')

    mergerequest_jsonld = activity['object']
    mergerequest = model.from_uri(database, mergerequest_jsonld['id'])

    if not mergerequest:
        log.debug('MergeRequest does not exist: {}'.format(mergerequest_jsonld['id']))
        return

    actor = model.Person.test_or_set(database, activity['actor']['id'])
    new_title = activity['result']['summary'] if 'summary' in activity['result'] else None
    new_content = activity['result']['content'] if 'content' in activity['result'] else None

    # Edit the pagure PullRequest object
    mergerequest.title = new_title
    mergerequest.initial_comment = new_content

    log.debug('MergeRequest updated: {}'.format(mergerequest_jsonld['id']))

@pattern({
    'type': 'Update',
    'id': _,
    'actor': {
        'id': _
    },
    'object': {
        'type': 'MergeRequest',
        'id': _remote
    },
    'result': {}
})
def update_remote_mergerequest(database, activity):
    """
    Remote user has updated a remote MergeRequest not belonging to this instance.
    """

    log.debug('Handling incoming Update(MergeRequest) Activity.')

    mergerequest_jsonld = activity['object']
    mergerequest = model.from_uri(database, mergerequest_jsonld['id'])

    if not mergerequest:
        log.debug('MergeRequest does not exist: {}'.format(mergerequest_jsonld['id']))
        return

    actor = model.Person.test_or_set(database, activity['actor']['id'])
    new_title = activity['result']['summary'] if 'summary' in activity['result'] else None
    new_content = activity['result']['content'] if 'content' in activity['result'] else None

    # Edit the pagure PullRequest object
    mergerequest.title = new_title
    mergerequest.initial_comment = new_content

    log.debug('MergeRequest updated: {}'.format(mergerequest_jsonld['id']))

@pattern({
    'type': 'Resolve',
    'id': _,
    'actor': {
        'id': _
    },
    'object': {
        'type': 'MergeRequest',
        'id': _local
    },
})
def merge_local_mergerequest(database, activity):
    """
    Remote user has merged a MergeRequest belonging to this instance.
    """

    pass

@pattern({
    'type': 'Resolve',
    'id': _,
    'actor': {
        'id': _
    },
    'object': {
        'type': 'MergeRequest',
        'id': _remote,
    },
})
def merge_remote_mergerequest(database, activity):
    """
    Remote user has merged a remote MergeRequest not belonging to this instance.
    """

    log.debug('Handling incoming Resolve(MergeRequest) Activity.')

    mergerequest = model.from_uri(database, activity['object']['id'])

    # Nothing to do because we don't have any local MergeRequest tracking
    # this remote object
    if not mergerequest:
        log.debug('Untracked MergeRequest has been merged: {}'.format(activity['object']['id']))
        return

    actor = model.Person.test_or_set(database, activity['actor']['id'])

    # Edit the pagure PullRequest object
    pagure.lib.query.close_pull_request(
        session=database,
        request=mergerequest,
        user=actor.username,
        merged=True)

    log.debug('MergeRequest merged: {}'.format(activity['object']['id']))

@pattern({
    'type': 'Close',
    'id': _,
    'actor': {
        'id': _remote
    },
    'object': {
        'type': 'MergeRequest',
        'id': _local
    },
})
def close_local_mergerequest(database, activity):
    """
    Remote user has closed a MergeRequest belonging to this instance.
    """

    log.debug('Handling incoming Close(MergeRequest) Activity.')

    mergerequest = model.from_uri(database, activity['object']['id'])

    # Nothing to do because we don't have any local MergeRequest with this URI
    if not mergerequest:
        log.debug('Remote user {} tried to close nonexistent MergeRequest {}'.format(activity['actor']['id'], activity['object']['id']))
        return

    actor = model.Person.test_or_set(database, activity['actor']['id'])

    # Check if the remote actor has the right to close the MergeRequest
    if mergerequest.user.remote_uri != actor.remote_uri:
        log.debug('Remote actor {} cannot close local MergeRequest {}'.format(actor.remote_uri, mergerequest.local_uri))
        return

    # Edit the pagure PullRequest object
    pagure.lib.query.close_pull_request(
        session=database,
        request=mergerequest,
        user=actor.username,
        merged=False)

    log.debug('MergeRequest closed: {}'.format(activity['object']['id']))

@pattern({
    'type': 'Close',
    'id': _,
    'actor': {
        'id': _remote
    },
    'object': {
        'type': 'MergeRequest',
        'id': _remote
    },
})
def close_remote_mergerequest(database, activity):
    """
    Remote user has closed a remote MergeRequest not belonging to this instance.
    """

    log.debug('Handling incoming Close(MergeRequest) Activity.')

    mergerequest = model.from_uri(database, activity['object']['id'])

    # Nothing to do because we don't have any local MergeRequest tracking
    # this remote object
    if not mergerequest:
        log.debug('Untracked MergeRequest has been closed: {}'.format(activity['object']['id']))
        return

    actor = model.Person.test_or_set(database, activity['actor']['id'])

    # Edit the pagure PullRequest object
    pagure.lib.query.close_pull_request(
        session=database,
        request=mergerequest,
        user=actor.username,
        merged=False)

    log.debug('MergeRequest closed: {}'.format(activity['object']['id']))

@pattern({
    'type': 'Reopen',
    'id': _,
    'actor': {
        'id': _remote,
    },
    'object': {
        'type': 'MergeRequest',
        'id': _local
    },
})
def reopen_local_mergerequest(database, activity):
    """
    Remote user has reopened a MergeRequest belonging to this instance.
    """

    log.debug('Handling incoming Reopen(MergeRequest) Activity.')

    mergerequest = model.from_uri(database, activity['object']['id'])

    # Nothing to do because we don't have any local MergeRequest with this URI
    if not mergerequest:
        log.debug('Remote user {} tried to reopen nonexistent MergeRequest {}'.format(activity['actor']['id'], activity['object']['id']))
        return

    actor = model.Person.test_or_set(database, activity['actor']['id'])

    # Check if the remote actor has the right to reopen the MergeRequest
    if mergerequest.user.remote_uri != actor.remote_uri:
        log.debug('Remote actor {} cannot reopen local MergeRequest {}'.format(actor.remote_uri, mergerequest.local_uri))
        return

    # Edit the pagure PullRequest object
    pagure.lib.query.reopen_pull_request(
        session=database,
        request=mergerequest,
        user=actor.username)

    log.debug('MergeRequest reopened: {}'.format(activity['object']['id']))

@pattern({
    'type': 'Reopen',
    'id': _,
    'actor': {
        'id': _remote
    },
    'object': {
        'type': 'MergeRequest',
        'id': _remote
    },
})
def reopen_remote_mergerequest(database, activity):
    """
    Remote user has reopened a remote MergeRequest not belonging to this instance.
    """

    log.debug('Handling incoming Reopen(MergeRequest) Activity.')

    mergerequest = model.from_uri(database, activity['object']['id'])

    # Nothing to do because we don't have any local MergeRequest tracking
    # this remote object
    if not mergerequest:
        log.debug('Untracked MergeRequest has been reopened: {}'.format(activity['object']['id']))
        return

    actor = model.Person.test_or_set(database, activity['actor']['id'])

    # Edit the pagure PullRequest object
    pagure.lib.query.reopen_pull_request(
        session=database,
        request=mergerequest,
        user=actor.username)

    log.debug('MergeRequest reopened: {}'.format(activity['object']['id']))

@pattern({
    'type': 'Create',
    'id': _,
    'actor': {
        'id': _
    },
    'object': {
        'type': 'Note',
        'id': _remote,
        'context': {
            'type': 'MergeRequest',
            'id': _
        },
        'attributedTo': {
            'id': _
        },
        'content': _
    },
})
def create_mergerequest_comment(database, activity):
    """
    Remote user has created a new TicketComment for one of our TicketTracker.
    """

    log.debug('Handling incoming Create(Note(MergeRequest)) Activity.')

    note = activity['object']

    # Check if we already have a local object for this remote comment
    if database.query(
           database.query(model.SameAs)
                   .filter(model.SameAs.remote_uri == note['id']).exists()
       ).scalar():

        log.debug('The note {} is already stored in the database. Will not create a new one.'.format(note['id']))
        return

    author = model.Person.test_or_set(database, note['attributedTo']['id'])

    # If the "context" of the Note is a local URL, it means somebody has created a
    # Note for a Repository in our instance. This is the case for example when
    # a remote user is contributing a comment to a local project.
    if note['context']['id'].startswith(APP_URL):

        mergerequest = model.from_uri(database, note['context']['id'])

    # If the "context" of the Note is not a local Ticket, this note was created
    # for a remote object. This is the case for example when a user has sent a
    # comment to a remote ticket, and we have received the Activity notification
    # because we are following that ticket.
    else:

        # Check if there is any local Ticket in the pagure database that is used
        # to track this remote Ticket
        mergerequest = model.from_uri(database, note['context']['id'])

        # This is a new Note for a remote Ticket
        if not mergerequest:
            log.debug('No local MergeRequest for {}'.format(note['context']['id']))
            return

    # Create the new comment to the ticket
    pagure.lib.query.add_pull_request_comment(
        session=database,
        request=mergerequest,
        commit=None,
        tree_id=None,
        filename=None,
        row=None,
        comment=note['content'],
        user=author.username)


